# -*- coding: utf-8 -*-
"""
xfree.user.models模块
这个模块提供了用户相关的模型

:copyright: (c) 2014 by the xfree Team.
:license: BSD, 查看LICENSE获取更多细节
"""

import logging

# 从Flask框架中导入用于生成URL的函数
from flask import url_for
# 导入Flask - Login中的匿名用户混合类和用户混合类
from flask_login import AnonymousUserMixin, UserMixin
# 导入用于处理密码哈希的函数（检查密码哈希和生成密码哈希）
from werkzeug.security import check_password_hash, generate_password_hash

# 导入自定义的已弃用功能相关的模块
from xfree.utils.deprecation import deprecated
# 导入自定义的认证错误异常类
from xfree.utils.exceptions import AuthenticationError
# 导入缓存和数据库扩展相关的模块
from xfree.extensions import cache, db
# 导入论坛相关的模型（论坛、帖子、主题、主题跟踪器）
from xfree.forum.models import Forum, Post, Topic, topictracker
# 导入自定义的数据库操作混合类和UTC日期时间类，以及用于使类可比较的函数
from xfree.utils.database import CRUDMixin, UTCDateTime, make_comparable
# 导入用于获取当前UTC时间的函数
from xfree.utils.helpers import time_utcnow
# 导入配置相关的模块
from xfree.utils.settings import xfree_config

# 获取名为当前模块名（xfree.user.models）的日志记录器
logger = logging.getLogger(__name__)


# 定义一个数据库表关联关系，用于多对多关联用户和用户组
groups_users = db.Table(
    "groups_users",
    # 用户ID列，外键关联到users表的id列，级联删除，不允许为空
    db.Column(
        "user_id",
        db.Integer,
        db.ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
    ),
    # 用户组ID列，外键关联到groups表的id列，级联删除，不允许为空
    db.Column(
        "group_id",
        db.Integer,
        db.ForeignKey("groups.id", ondelete="CASCADE"),
        nullable=False,
    ),
)


# 使用make_comparable装饰器使这个类可比较
@make_comparable
class Group(db.Model, CRUDMixin):
    # 数据库表名
    __tablename__ = "groups"

    # 用户组ID，整数类型，主键
    id = db.Column(db.Integer, primary_key=True)
    # 用户组名称，字符串类型，长度255，唯一且不允许为空
    name = db.Column(db.String(255), unique=True, nullable=False)
    # 用户组描述，文本类型，允许为空
    description = db.Column(db.Text, nullable=True)

    # 用户组类型相关的布尔属性
    # 是否是管理员组
    admin = db.Column(db.Boolean, default=False, nullable=False)
    # 是否是超级版主组
    super_mod = db.Column(db.Boolean, default=False, nullable=False)
    # 是否是版主组
    mod = db.Column(db.Boolean, default=False, nullable=False)
    # 是否是访客组
    guest = db.Column(db.Boolean, default=False, nullable=False)
    # 是否是被封禁组
    banned = db.Column(db.Boolean, default=False, nullable=False)

    # 版主权限相关的布尔属性（仅当用户是版主时可用）
    # 是否有权限编辑用户信息
    mod_edituser = db.Column(db.Boolean, default=False, nullable=False)
    # 是否有权限封禁用户
    mod_banuser = db.Column(db.Boolean, default=False, nullable=False)

    # 用户权限相关的布尔属性
    # 是否有权限编辑帖子
    editpost = db.Column(db.Boolean, default=True, nullable=False)
    # 是否有权限删除帖子
    deletepost = db.Column(db.Boolean, default=False, nullable=False)
    # 是否有权限删除主题
    deletetopic = db.Column(db.Boolean, default=False, nullable=False)
    # 是否有权限发布主题
    posttopic = db.Column(db.Boolean, default=True, nullable=False)
    # 是否有权限回复帖子
    postreply = db.Column(db.Boolean, default=True, nullable=False)
    # 是否有权限查看隐藏内容
    viewhidden = db.Column(db.Boolean, default=False, nullable=False)
    # 是否有权限使内容隐藏
    makehidden = db.Column(db.Boolean, default=False, nullable=False)

    # 方法
    def __repr__(self):
        """
        设置为数据库中对象的唯一键表示。
        这是cache.memoize()在跨请求工作时所需要的。
        """
        return "<{} {} {}>".format(self.__class__.__name__, self.id, self.name)

    @classmethod
    def selectable_groups_choices(cls):
        """
        查询并返回按名称升序排列的用户组选择项，
        选择项包含用户组的ID和名称。
        """
        return (
            Group.query.order_by(Group.name.asc())
           .with_entities(Group.id, Group.name)
           .all()
        )

    @classmethod
    def get_guest_group(cls):
        """
        查询并返回访客用户组。
        """
        return cls.query.filter(cls.guest == True).first()

    @classmethod
    def get_member_group(cls):
        """
        返回第一个成员用户组。
        （这里感觉代码有点不太优雅）
        """
        return cls.query.filter(
            cls.admin == False,
            cls.super_mod == False,
            cls.mod == False,
            cls.guest == False,
            cls.banned == False,
        ).first()


class User(db.Model, UserMixin, CRUDMixin):
    # 数据库表名
    __tablename__ = "users"

    # 用户ID，整数类型，主键
    id = db.Column(db.Integer, primary_key=True)
    # 用户名，字符串类型，长度200，唯一且不允许为空
    username = db.Column(db.String(200), unique=True, nullable=False)
    # 用户电子邮件，字符串类型，长度200，唯一且不允许为空
    email = db.Column(db.String(200), unique=True, nullable=False)
    # 存储用户密码的哈希值，列名是"password"，字符串类型，长度120，不允许为空
    _password = db.Column("password", db.String(120), nullable=False)
    # 用户加入日期，使用自定义的UTC日期时间类型，默认值为当前UTC时间，不允许为空
    date_joined = db.Column(
        UTCDateTime(timezone=True), default=time_utcnow, nullable=False
    )
    # 用户最后一次出现时间，使用自定义的UTC日期时间类型，默认值为当前UTC时间，允许为空
    lastseen = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=True)
    # 用户生日，日期时间类型，允许为空
    birthday = db.Column(db.DateTime, nullable=True)
    # 用户性别，字符串类型，长度10，允许为空
    gender = db.Column(db.String(10), nullable=True)
    # 用户个人网站，字符串类型，长度200，允许为空
    website = db.Column(db.String(200), nullable=True)
    # 用户所在地，字符串类型，长度100，允许为空
    location = db.Column(db.String(100), nullable=True)
    # 用户签名，文本类型，允许为空
    signature = db.Column(db.Text, nullable=True)
    # 用户头像链接，字符串类型，长度200，允许为空
    avatar = db.Column(db.String(200), nullable=True)
    # 用户备注，文本类型，允许为空
    notes = db.Column(db.Text, nullable=True)

    # 用户最后一次登录失败时间，使用自定义的UTC日期时间类型，允许为空
    last_failed_login = db.Column(UTCDateTime(timezone=True), nullable=True)
    # 用户登录尝试次数，整数类型，默认值为0，不允许为空
    login_attempts = db.Column(db.Integer, default=0, nullable=False)
    # 用户账户是否已激活，布尔类型，默认值为False，不允许为空
    activated = db.Column(db.Boolean, default=False, nullable=False)

    # 用户主题风格，字符串类型，长度15，允许为空
    theme = db.Column(db.String(15), nullable=True)
    # 用户语言偏好，字符串类型，默认值为"en"，允许为空
    language = db.Column(db.String(15), default="en", nullable=True)

    # 用户发布的帖子数量，整数类型，默认值为0
    post_count = db.Column(db.Integer, default=0)

    # 用户所属的主要用户组ID，外键关联到groups表的id列，不允许为空
    primary_group_id = db.Column(db.Integer, db.ForeignKey("groups.id"), nullable=False)

    # 与用户发布的帖子建立关系，反向引用为"user"，连接条件是用户ID等于帖子的用户ID，加载方式为动态加载
    posts = db.relationship(
        "Post", backref="user", primaryjoin="User.id == Post.user_id", lazy="dynamic"
    )

    # 与用户创建的主题建立关系，反向引用为"user"，连接条件是用户ID等于主题的用户ID，加载方式为动态加载
    topics = db.relationship(
        "Topic", backref="user", primaryjoin="User.id == Topic.user_id", lazy="dynamic"
    )

    # 与用户的主要用户组建立关系，反向引用为"user_group"，一对一关系，加载方式为立即加载，外键为primary_group_id
    primary_group = db.relationship(
        "Group",
        backref="user_group",
        uselist=False,
        lazy="joined",
        foreign_keys=[primary_group_id],
    )

    # 与用户的次要用户组建立关系，通过中间表groups_users关联，连接条件是中间表的user_id等于用户ID，反向引用为"users"，加载方式为动态加载
    secondary_groups = db.relationship(
        "Group",
        secondary=groups_users,
        primaryjoin=(groups_users.c.user_id == id),
        backref=db.backref("users", lazy="dynamic"),
        lazy="dynamic",
    )

    # 与用户跟踪的主题建立关系，通过中间表topictracker关联，连接条件是中间表的user_id等于用户ID，反向引用为"topicstracked"，加载方式为动态加载，单一父对象
    tracked_topics = db.relationship(
        "Topic",
        secondary=topictracker,
        primaryjoin=(topictracker.c.user_id == id),
        backref=db.backref("topicstracked", lazy="dynamic"),
        lazy="dynamic",
        single_parent=True,
    )

    # 属性
    @property
    def is_active(self):
        """
        返回账户的状态。
        如果“ACTIVATE_ACCOUNT”选项被禁用，它将总是返回True。
        如果该选项被启用，将根据账户状态返回True或False。
        """
        if xfree_config["ACTIVATE_ACCOUNT"]:
            if self.activated:
                return True
            return False

        return True

    @property
    def last_post(self):
        """
        返回用户最新发布的帖子。
        """
        return (
            Post.query.filter(Post.user_id == self.id)
           .order_by(Post.date_created.desc())
           .first()
        )

    @property
    def url(self):
        """
        返回用户的个人资料页面URL。
        """
        return url_for("user.profile", username=self.username)

    @property
    def permissions(self):
        """
        返回用户的权限。
        """
        return self.get_permissions()

    @property
    def groups(self):
        """
        返回用户所属的用户组。
        """
        return self.get_groups()

    @property
    def days_registered(self):
        """
        返回用户注册的天数。
        """
        days_registered = (time_utcnow() - self.date_joined).days
        if not days_registered:
            return 1
        return days_registered

    @property
    def topic_count(self):
        """
        返回用户创建的主题数量。
        """
        return Topic.query.filter(Topic.user_id == self.id).count()

    @property
    def posts_per_day(self):
        """
        返回用户每天平均发布的帖子数量。
        """
        return round((float(self.post_count) / float(self.days_registered)), 1)

    @property
    def topics_per_day(self):
        """
        返回用户每天平均创建的主题数量。
        """
        return round((float(self.topic_count) / float(self.days_registered)), 1)

    # 方法
    def __repr__(self):
        """
        设置为数据库中对象的唯一键表示。
        这是cache.memoize()在跨请求工作时所需要的。
        """
        return "<{} {}>".format(self.__class__.__name__, self.username)

    def _get_password(self):
        """
        返回哈希后的密码。
        """
        return self._password

    def _set_password(self, password):
        """
        为提供的密码生成密码哈希值。
        如果密码为空，则不进行操作。
        """
        if not password:
            return
        self._password = generate_password_hash(password)

    # 通过只暴露password字段来隐藏密码加密过程
    password = db.synonym(
        "_password", descriptor=property(_get_password, _set_password)
    )

    def check_password(self, password):
        """
        检查密码。如果密码匹配返回True，否则返回False。
        如果用户密码为空，则返回False。
        """
        if self.password is None:
            return False
        return check_password_hash(self.password, password)

    @classmethod
    @deprecated("Use authentication services instead.")
    def authenticate(cls, login, password):
        """
        用于认证用户的类方法。
        如果用户/密码组合正确，它将返回用户对象。
        如果用户多次输入错误密码，他将在指定时间内被锁定账户。

        :param login: 可以是用户名或电子邮件地址。
        :param password: 与用户名或电子邮件相关联的密码。
        """
        user = cls.query.filter(
            db.or_(User.username == login, User.email == login)
        ).first()

        if user is not None:
            if user.check_password(password):
                # 成功登录后重置登录尝试次数
                user.login_attempts = 0
                user.save()
                return user

            # 用户存在，但密码错误
            # 之前没有错误登录记录
            if user.login_attempts is None:
                user.login_attempts = 1
            else:
                user.login_attempts += 1
            user.last_failed_login = time_utcnow()
            user.save()

        # 防止账户枚举定时攻击
        check_password_hash("dummy password", password)

        raise AuthenticationError

    def recalculate(self):
        """
        重新计算用户的帖子数量。
        """
        self.post_count = Post.query.filter_by(user_id=self.id).count()
        self.save()
        return self

    def all_topics(self, page, viewer):
        """
        返回由给定用户创建的主题，按最新的排在前面。

        :param page: 要显示的页面。
        :param viewer: 查看页面的用户。只返回查看者有权访问的帖子。
        :rtype: flask_sqlalchemy.Pagination
        """
        group_ids = [g.id for g in viewer.groups]
        topics = (
            Topic.query.filter(
                Topic.user_id == self.id,
                Forum.id == Topic.forum_id,
                Forum.groups.any(Group.id.in_(group_ids)),
            )
           .order_by(Topic.id.desc())
           .paginate(
                page=page, per_page=xfree_config["TOPICS_PER_PAGE"], error_out=False
            )
        )
        return topics

    def all_posts(self, page, viewer):
        """
        返回由给定用户发布的帖子，按最新的排在前面。

        :param page: 要显示的页面。
        :param viewer: 查看页面的用户。只返回查看者有权访问的帖子。
        :rtype: flask_sqlalchemy.Pagination
        """
        group_ids = [g.id for g in viewer.groups]
        posts = (
            Post.query.filter(
                Post.user_id == self.id,
                Post.topic_id == Topic.id,
                Topic.forum_id == Forum.id,
                Forum.groups.any(Group.id.in_(group_ids)),
            )
           .order_by(Post.id.desc())
           .paginate(
                page=page, per_page=xfree_config["TOPICS_PER_PAGE"], error_out=False
            )
        )
        return posts

    def track_topic(self, topic):
        """
        跟踪指定的主题。

        :param topic: 应该添加到主题跟踪器中的主题。
        """
        if not self.is_tracking_topic(topic):
            self.tracked_topics.append(topic)
            return self

    def untrack_topic(self, topic):
        """
        取消跟踪指定的主题。

        :param topic: 应该从主题跟踪器中移除的主题。
        """
        if self.is_tracking_topic(topic):
            self.tracked_topics.remove(topic)
            return self

    def is_tracking_topic(self, topic):
        """
        检查用户是否已经在跟踪这个主题。

        :param topic: 要检查的主题。
        """
        return (
            self.tracked_topics.filter(topictracker.c.topic_id == topic.id).count() > 0
        )

    def add_to_group(self, group):
        """
        如果用户不在该组中，则将用户添加到`group`。

        :param group: 应该添加到用户的组。
        """
        if not self.in_group(group):
            self.secondary_groups.append(group)
            return self

    def remove_from_group(self, group):
        """
        如果用户在该组中，则将用户从`group`中移除。

        :param group: 应该从用户中移除的组。
        """
        if self.in_group(group):
            self.secondary_groups.remove(group)
            return self

    def in_group(self, group):
        """
        如果用户在指定的组中，则返回True。

        :param group: 要检查的组。
        """
        return (
            self.secondary_groups.filter(groups_users.c.group_id == group.id).count()
            > 0
        )

    @cache.memoize()
    def get_groups(self):
        """
        返回用户所属的所有组。
        """
        return [self.primary_group] + list(self.secondary_groups)

    @cache.memoize()
    def get_permissions(self, exclude=None):
        """
        返回一个包含用户所有权限的字典

        :param exclude: 要排除的属性列表，如果为None，则默认排除一些属性
        """
        if exclude:
            exclude = set(exclude)
        else:
            exclude = set()
        exclude.update(["id", "name", "description"])

        perms = {}
        # 获取访客组
        for group in self.groups:
            columns = set(group.__table__.columns.keys()) - set(exclude)
            for c in columns:
                perms[c] = getattr(group, c) or perms.get(c, False)
        return perms

    def invalidate_cache(self):
        """
        使该对象的缓存元数据失效。
        """
        cache.delete_memoized(self.get_permissions, self)
        cache.delete_memoized(self.get_groups, self)

    def ban(self):
        """
        封禁用户。成功则返回True。
        """
        if not self.get_permissions()["banned"]:
            banned_group = Group.query.filter(Group.banned == True).first()

            self.primary_group = banned_group
            self.save()
            self.invalidate_cache()
            return True
        return False

    def unban(self):
        """
        解封用户。成功则返回True。
        """
        if self.get_permissions()["banned"]:
            member_group = Group.query.filter(
                Group.admin == False,
                Group.super_mod == False,
                Group.mod == False,
                Group.guest == False,
                Group.banned == False,
            ).first()

            self.primary_group = member_group
            self.save()
            self.invalidate_cache()
            return True
        return False

    def save(self, groups=None):
        """
        保存用户。如果提供了用户组列表，它将把这些组添加到用户的次要用户组中。

        :param groups: 应该添加到用户次要用户组的用户组列表。
        """
        if groups is not None:
            # TODO: 仅移除/添加选中的组
            with db.session.no_autoflush:
                secondary_groups = self.secondary_groups.all()
                for group in secondary_groups:
                    self.remove_from_group(group)

            for group in groups:
                # 不要将主要用户组添加到次要用户组中
                if group == self.primary_group:
                    continue
                self.add_to_group(group)

            self.invalidate_cache()

        db.session.add(self)
        db.session.commit()
        return self

    def delete(self):
        """
        删除用户。
        """
        db.session.delete(self)
        db.session.commit()

        return self


class Guest(AnonymousUserMixin):
    # 权限属性
    @property
    def permissions(self):
        return self.get_permissions()

    # 用户组属性
    @property
    def groups(self):
        return self.get_groups()

    # 获取用户组，使用缓存装饰器
    @cache.memoize()
    def get_groups(self):
        # 查询并返回所有访客用户组
        return Group.query.filter(Group.guest == True).all()

    # 获取权限，使用缓存装饰器
    @cache.memoize()
    def get_permissions(self, exclude=None):
        """
        返回包含用户所有权限的字典
        :param exclude: 要排除的属性列表，如果为None，则默认排除一些属性
        """
        if exclude:
            exclude = set(exclude)
        else:
            exclude = set()
        exclude.update(["id", "name", "description"])

        perms = {}
        # 获取访客组
        for group in self.groups:
            columns = set(group.__table__.columns.keys()) - set(exclude)
            for c in columns:
                perms[c] = getattr(group, c) or perms.get(c, False)
        return perms

    # 使缓存元数据失效的类方法
    @classmethod
    def invalidate_cache(cls):
        """
        使该对象的缓存元数据失效。
        """
        cache.delete_memoized(cls.get_permissions, cls)
